package dodo.mindmap;

import java.awt.Color;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import javax.swing.text.Position;

import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import processing.core.PApplet;
import processing.core.PConstants;

import geomerative.RPoint;
import geomerative.RStyle;

// TODO: when everything goes to NAN, assign a random layout and start again

public class Mindmap implements PConstants {
	
	static float RESTITUTION = (float) 0.25;
	
	ConcurrentHashMap<Integer, Node> nodes;
	ConcurrentHashMap<Integer,Link> contextual_links;
	Vector<Link> hierarchial_links;
	String baseurl;
	Node base,focus;
	MApplet parent;
	boolean dophysics = true, updated;
	MindmapState state;
	RStyle bgStyle;
	
	public Mindmap(MApplet parent, String baseurl){
		nodes = new ConcurrentHashMap<Integer,Node>();
		contextual_links = new ConcurrentHashMap<Integer,Link>();
		hierarchial_links = new Vector<Link>();
		
		this.updated = false;
		this.focus = null;
		this.baseurl = baseurl;
		
		this.parent = parent;
		this.parent.registerMouseEvent(this);
		
		this.state = MindmapState.STEADY;
		
		this.bgStyle = NodeStyle.background;
		
		updateMap();
		
		if(nodes.isEmpty()){
			String title = "What is waste?";
			String content = "How do you define waste? How can we deal with waste in a way that is harmonious with the natural environment?\n\nNew -> record a new idea\nConnect -> connect two ideas together";
		
			Node n = new Node(this,new RPoint(parent.screen.width/2,parent.screen.height/2), new RPoint(200,200), 100000, NodeStyle.basenode, NodeStyle.basenodeText, title, content);
			Node.sendCreateNode(n);
			
			setBase(n);
		}
	}
	
	public void draw(){
		parent.background(bgStyle.fillColor);
		parent.fill(0);
		
		if( dophysics )
			nodePhysics();
		
		//Center shit
		RPoint center = new RPoint(parent.screen.width/2,parent.screen.height/2);
		center.sub(base.position);
		for(Node n:nodes.values()){
			n.position.add(center);
			if(Float.isNaN(n.position.norm())){
				//reset();
				if( n != base )
					positionNode(n, n.getDBParent());
				else
					n.position = center;
			}
			
			if(Float.isNaN(n.velocity.norm()) && !updated){
				n.velocity = new RPoint(10*new Random().nextFloat(), 10*new Random().nextFloat());
				updated = true;
			}else{
				updated = false;
			}
		}
		
		//Draw all the links
		for(Link l:contextual_links.values())
			l.draw();
		for(Link l:hierarchial_links)
			l.draw();
		
		//Draw all the nodes
		for(Node n:nodes.values()){
			if( n != focus )
				n.draw();
		}
		
		if( focus != null ) // Draw focus on top
			focus.draw();
		
		if((Calendar.getInstance().get(Calendar.SECOND))%10 == 0){
			
			updateMap();
		}
	}
	
	public void reset(){
		nodes.clear();
		contextual_links.clear();
		hierarchial_links.clear();
		
		updateMap();
	}
	
	public void setBase(Node base){
		nodes.put(base.id, base);
		this.base = base;
	}
	
	protected void nodePhysics(){
		float damping = 0.9f;
		float timestep = 0.1f;//0.1;
		float k_spring = 250;
		float k_coulomb = 40000;
		
		RPoint net_force;
		double total_kinetic_energy = 0; // running sum of total kinetic energy over all particles
		
	    for(Node node:nodes.values()){
	    	net_force = new RPoint(0,0); // running sum of total force on this particular node
	    	
	    	for(Node n:nodes.values()){
	    		if( node != n ){
	    			RPoint force = new RPoint(node.position);
	    			force.sub(n.position);
	    			float dist = force.norm();
	    			force.normalize();
	    			force.scale(k_coulomb * node.getCharge() * n.getCharge()/PApplet.pow(dist,2));
	    			net_force.add(force);
	    		}
	    	}
    		
	    	for(Link l:node.links){
    			RPoint force = new RPoint(node.position);
    			force.sub(l.getTo(node).position);
    			force.scale(-k_spring * l.k);
    			net_force.add(force);
    		}
	    	
	    	//without damping, it moves forever
	    	net_force.scale(timestep / node.getMass());
	    	//node.velocity.add(net_force);
	    	node.velocity = new RPoint(net_force);
	    	node.velocity.scale(damping);

	    	//TODO: Try checking for upcoming collisions and calculating avoidances here!
    		LinkedList<Node> queue = new LinkedList<Node>();
    		LinkedList<Float> timesteps = new LinkedList<Float>();
    		queue.offer(node);
    		timesteps.offer(timestep);
	    	
    		while(!queue.isEmpty()){
    			Node n = queue.poll();
    			float t = timesteps.poll();
    			
    			RPoint to = new RPoint(n.velocity);
    			to.scale(t);
    			to.add(n.position);
    			
		    	Node collide = Node.pathCollision(n, new RPoint(n.position), to, new Vector<Node>(nodes.values()));
		    	if( collide != null ){
		    		/*
		    		 * Calculate how far along the path the collision is.
		    		 * Simulate the collision at that point and then continue to move and check for collisions over the course of the timeframe
		    		 * For each collision put the two balls into a queue.
		    		 */
		    		//TODO: Fix Collision while growing
		    		RPoint path = new RPoint(n.velocity);
		    		path.scale(t);
		    		
		    		RPoint parallel = new RPoint(path);
		    		parallel.normalize();
		    		
		    		RPoint norm = new RPoint(-parallel.y, parallel.x);
		    		RPoint diff = new RPoint(collide.position);
		    		diff.sub(n.position);
		    		
					float proj_norm = diff.mult(norm);
					float p_dist = PApplet.sqrt(PApplet.sq((n.getSize().x + collide.getSize().x)/2) - PApplet.sq(proj_norm));
					diff.scale(-1);
					float translate = PApplet.abs(diff.mult(parallel))-p_dist;
					
					// What percent of the timeframe has gone by
					float pct = 1;
					if(path.norm() != 0)
						pct = translate / path.norm();
					
					// Move the node to the collision point
					parallel.scale(translate);
					n.position.add(parallel);
					
					// Simulate the collision
					simulateCollision(n, collide, damping);
		    		
					queue.add(n);
					queue.add(collide);
					timesteps.add(t * pct);
					timesteps.add(t * pct);
		    	}else if(n.velocity.norm() > 1){
		    		n.position = new RPoint(to);
		    	}
    		}
	    }
	    
	    boolean node_life = true;
	    
	    String s = "";
	    
	    for(Node n:nodes.values()){
	    	s += n.title + " " + n.velocity.norm() + '\n';
	    	total_kinetic_energy += n.getMass() * PApplet.pow(n.velocity.norm(), 2);
	    	node_life = node_life && (n.state == NodeState.LIFE);
	    }
	    
	    if( total_kinetic_energy < 0.005 && node_life ){
	    	dophysics = false;
	    }
	}
	
	protected void simulateCollision(Node n1, Node n2, float damping){
		/*
		 * Changes velocities of the nodes in collision
		 */
		RPoint normal, v1_norm, v2_norm;
		
		// Set up the new axes
		normal = new RPoint(n1.position);
		normal.sub(n2.position);
		normal.normalize();
		
		// Isolate the normal components velocity
		v1_norm = new RPoint(normal); 
		v1_norm.scale(n1.velocity.mult(normal));
		v2_norm = new RPoint(normal); 
		v2_norm.scale(n2.velocity.mult(normal));
		
		// Subtract the normal components to isolate the tangential components - they don't change 
		n1.velocity.sub(v1_norm);
		n2.velocity.sub(v2_norm);
		
		// Add the new normal components back to the velocity
		float mass_sum = n1.getMass() + n2.getMass();
		float mass_diff = n1.getMass() - n2.getMass();
		
		RPoint n1_1 = new RPoint(v1_norm);
		RPoint n1_2 = new RPoint(v2_norm);
		RPoint n2_1 = new RPoint(v2_norm);
		RPoint n2_2 = new RPoint(v1_norm);
		
		n1_1.scale(mass_diff);
		n1_2.scale(2 * n2.getMass());
		n2_1.scale(-mass_diff);
		n2_2.scale(2 * n1.getMass());
		
		n1_1.add(n1_2);
		n1_1.scale(1/mass_sum);
		
		n2_1.add(n2_2);
		n2_1.scale(1/mass_sum);
		
		n1.velocity.add(n1_1);
		n2.velocity.add(n2_1);
		
		if( n1.state == NodeState.LIFE )
			n1.velocity.scale(damping);
		else
			n1.velocity.scale(1/damping);
		
		if( n2.state == NodeState.LIFE )
			n2.velocity.scale(damping);
		else
			n2.velocity.scale(1/damping);
			
	}
	
	public Node getClicked(float f, float g){
		for( Node n: nodes.values() ){
			if( n.clicked(f, g) )
				return n;
		}
		return null;
	}
	
	public Node getClicked(RPoint p){
		return getClicked(p.x,p.y);
	}
	
	public void mouseEvent(MouseEvent e){
		switch (e.getID()) {
		case MouseEvent.MOUSE_CLICKED:

			dophysics = true;
			
			switch (state) {
			case LINKING:
				Node clicked = getClicked(e.getX(), e.getY());
				
				if( clicked != null){
					if( clicked != focus )
						doLinkNodes(focus, clicked, 0.5f); //TODO: Pull the link strength into a parameter
				}
				
				if( clicked != focus )
					doUnfocus();
				
				break;
				
			case FOCUSED:
				dophysics = false;
				
				if(!focus.clicked(e.getX(), e.getY())){
					if( focus.state == NodeState.CONCEPTION ){
						if(focus.getTitle().length() > 0){
							// Move the node
							Node parentnode = focus.getParent();
							
							focus.position = positionNode(focus, parentnode);
							
							Node.sendCreateNode(focus);
							doUnfocus();
						}else{
							for(Link l: focus.links){
								hierarchial_links.remove(l);
								contextual_links.remove(l);
							}
							
							nodes.remove(focus);
							doUnfocus();
						}
					}
					
					doUnfocus();
				}
				
				break;
				
			case STEADY:
				if( getClicked(e.getX(), e.getY()) != null ){
					focus = getClicked(e.getX(), e.getY());
					bgStyle = NodeStyle.backgroundFade;
					
					for(Node n:nodes.values()){
						if( n == focus )
							n.state = NodeState.FOCUSING;
						else
							n.state = NodeState.BLURRING;
					}
					state = MindmapState.FOCUSED;
					dophysics = false;
				}
				break;
			default:
				break;
			}
			break;

		default:
			break;
		}
	}
	
	public void doUnfocus(){
		for(Node n:nodes.values()){
			if( n.state == NodeState.FOCUSED )
				n.state = NodeState.UNFOCUSING;
			else if( n.state == NodeState.BLURRED )
				n.state = NodeState.UNBLURRING;
		}
		
		focus = null;
		state = MindmapState.STEADY;
		bgStyle = NodeStyle.background;
		dophysics = true;
	}
	
	public void doLinking(Node from){
		doUnfocus();
		state = MindmapState.LINKING;
		focus = from;
	}
	
	public void doMakeChildNode(Node parentnode, Node child){
		
		nodes.put(child.id, child);
		Link l = new Link(this, parentnode, child, 1);
		parentnode.links.add(l);
		child.links.add(l);
		hierarchial_links.add(l);
		
		parent.redraw();
		
		child.position = base.position;
		
		child.state = NodeState.CONCEIVING;
		doUnfocus();
		
		focus = child;
		state = MindmapState.FOCUSED;
		
		dophysics = false;
	}
	
	public void doLinkNodes(Node from, Node to, float strength){
		Link.sendCreateLink(new Link(this, from, to, strength));
		
		doUnfocus();
	}
	
	public String dbRequest(String type, String param){
		Vector<byte[]> hashes;
		if( type.equals("node") )
			hashes = getNodeHashes();
		else
			hashes = getLinkHashes();

		HttpClient httpclient = new DefaultHttpClient();
		HttpPost httppost = new HttpPost(baseurl + "/ideavolution/" + type + "/" + param + "/");
		String response;
		
		ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
		for( byte[] hash: hashes ){
			params.add(new BasicNameValuePair("a", String.valueOf(hash)));
		}
		
		try {
			UrlEncodedFormEntity encoded_params = new UrlEncodedFormEntity(params);
			httppost.setEntity(encoded_params);
			ResponseHandler<String> responseHandler = new BasicResponseHandler();
			response = httpclient.execute(httppost, responseHandler);
			
			return response;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
	
	public void updateMap(){
		dophysics = true;
		String response = dbRequest("node", "modified");

    	Hashtable<Integer, Node> positions = new Hashtable<Integer, Node>();
		//Parse the node ids out of the hash
		if( !response.equals("") ){
			for(int i=0; i < response.split(",").length; i++){
				int id = Integer.parseInt(response.split(",")[i]);
	        	Node target;
				
				// Check if the node exists
	        	if(nodes.containsKey(id)){
	        		target = nodes.get(id);
	        	}else{
	    			RPoint p = new RPoint(parent.screen.width/2,parent.screen.height/2);
	    			
	        		target = new Node(this, p, new RPoint(80,80), 100, NodeStyle.node, NodeStyle.nodeText);
	        		target.id = id;
	        		if( target.getDBParent() != null ){
	        			positions.put(target.id, target);
	        			// Create the link to the parent
		        		Link link = new Link(this, id, target, target.getDBParent(), 1);
		        		target.links.add(link);
		        		target.getDBParent().links.add(link);
		        		hierarchial_links.add(link);
	        		}else{
	        			base = target;
	        			target.mass = 100000;
	        			target.shapeStyle = NodeStyle.basenode;
	        			target.textStyle = NodeStyle.basenodeText;
	        			target.size = new RPoint(200,200);
	        		}
	        		
	        		nodes.put(target.id, target);
	        	}
	        	
	        	target.setSize(target.getDBSize());
	        	target.title = target.getDBTitle();
	        	target.content = target.getDBContent();
				
			}
		}

		// Process the links
		response = dbRequest("link", "modified");
		
		if( !response.equals("") ){
	    	for(int i=0; i < response.split(",").length; i++){
				int id = Integer.parseInt(response.split(",")[i]);
	        	if(contextual_links.containsKey(id)){
	        		contextual_links.get(id).k = contextual_links.get(id).getDBStrength();
	        	}else{
	        		Link link = new Link(this, id);
	        		link.from = link.getDBFrom();
	        		link.to = link.getDBTo();
	        		link.k = link.getDBStrength();
	        		link.from.links.add(link);
	        		link.to.links.add(link);
	        		contextual_links.put(id, link);
	        	}	        	
	        }
		}
        TreeSet<Integer> nodeids = new TreeSet<Integer>(positions.keySet());
        for(Integer nid: nodeids){
    		positions.get(nid).position = positionNode(positions.get(nid), positions.get(nid).getDBParent());
        }
	}
	
	public Vector<byte[]> getNodeHashes(){
		Vector<byte[]> sums = new Vector<byte[]>();
		
		for(Node n:nodes.values()){
			sums.add(n.getMD5());
		}
		
		return sums;
	}
	
	public Vector<byte[]> getLinkHashes(){
		Vector<byte[]> sums = new Vector<byte[]>();
		
		for(Link l:contextual_links.values()){
			sums.add(l.getMD5());
		}
		
		return sums;
	}
	
	class NodeComparator implements Comparator<Node> {
		public int compare(Node n1, Node n2) {
			RPoint d1 = new RPoint(base.position);
			RPoint d2 = new RPoint(base.position);
			
			d1.sub(n1.position);
			d2.sub(n2.position);
			
			if(d1.norm() < d2.norm())
				return 1;
			else if(d1.norm() > d2.norm())
				return -1;
			else if(n1.getSize().x > n2.getSize().x)
				return 1;
			else if(n1.getSize().x < n2.getSize().x)
				return -1;
			else
				return 0;
		}
		
		public boolean equals(Node n1, Node n2){
			RPoint d1 = new RPoint(base.position);
			RPoint d2 = new RPoint(base.position);
			
			d1.sub(n1.position);
			d2.sub(n2.position);
			
			if(d1.norm() == d2.norm() && n1.getSize().x == n2.getSize().x)
				return true;
			else
				return false;
		}
	}
	
	public RPoint positionNode(Node positionnode, Node parentnode){// Find an empty spot around the node
		float rad = (positionnode.size.x + parentnode.size.x) / 2 + 1;
		
		RPoint p = new RPoint(parentnode.position);
		p.add(new RPoint(rad, 0));
			
		p.rotate(TWO_PI * new Random().nextFloat(), parentnode.position);
		
		while(getClicked(p) != null){
			p.rotate((TWO_PI * new Random().nextFloat()) - PI, parentnode.position);
			p.sub(parentnode.position);
			p.scale(1.1f);
			p.add(parentnode.position);
		}
		
		return p;
	}
	
}
